﻿using System;
using System.ComponentModel;
using System.Globalization;
using System.Threading;
using OpenTap.Plugins.Interfaces.ACPsu;
using OpenTap.Plugins.Interfaces.Common;

namespace OpenTap.Plugins.AcPsu
{
    [Display("Ametek AST1501 AC PSU driver", Group: "OpenTap.Plugins", Description: "Ametek AST1501 AC PSU driver")]
    public class AcPsuAmetek : AcPsuBase
    {
        #region enums
        public enum EQuestionableStatRegBitMask
        {
            OverVoltage =      1, // VF over- or under-voltage protection has tripped
            OverCurrent =      2, // CF over-current protection has tripped
            OverTemperature =  8, // OT over-temperature protection has tripped
            Inhibit =        512, // RI remote inhibit is active
            RmsCurrent =    4096  // CL rms current limit is active
        }

        public enum ESense
        {
            [Scpi("INT")] Int,
            [Scpi("EXT")] Ext
        }

        public enum EVoltageRangeAmetekAc
        {
            RangeLoV = 200,
            RangeHiV = 400
        }

        public enum EVoltageRangeAmetekDc
        {
            RangeLoV = 250,
            RangeHiV = 500
        }
        #endregion

        public AcPsuAmetek()
        {
            // Initial parameters defaults with adding new test to TAP
            VoltageSenseInit = ESense.Int;                       // Default
            VoltRangeInit = (int)EVoltageRangeAmetekAc.RangeLoV; // Default
            RemoteInhibitModeInit = ERemoteInhibitMode.Latc;     // Default
            RemoteInhibitLevelInit = ERemoteInhibitLevel.High;   // Default
            Name = "AmetekAST1501";
        }

        #region Settings

        [Display( Name: "Voltage Range", Group: "Initial Parameters", Order: 1,
           Description: "Voltage Range to be initialized(HIGH/LOW). Can be changed later.")]
        public EVoltageRangeAmetekAc VoltageRange
        {
            get => (EVoltageRangeAmetekAc)VoltRangeInit;
            set => VoltRangeInit = (int)value;
        }

        [Display( Name: "Voltage Sense", Group: "Initial Parameters", Order: 2,
           Description: "Voltage Sense connection configuration: INT/EXT. \n" +
                        "If EXT, be sure that sense lines are correctly connected. \n" +
                        "Can NOT be changed with in running.")]
        public ESense VoltageSenseInit { get; set; }

        [Display( Name: "RI Mode", Group: "Initial Parameters", Order: 3,
           Description: "Remote Inhibit Mode configuration, which can NOT be changed with in running. \n" +
                         "LATC = A TTL low at the RI input latches the output in the protection shutdown state, which can only be cleared by SW. \n" +
                         "LIVE = The output state follows the state of the RI input. A TTL low at the RI input turns the output OFF. \n" +
                         "OFF  = The instrument ignores the RI input.")]
        public ERemoteInhibitMode RemoteInhibitModeInit { get; set; }

        [Display( Name: "RI Level", Group: "Initial Parameters", Order: 4,
           Description: "Remote Inhibit Level configuration (LOW/HIGH). \n" +
                        "Can NOT be changed with in running.")]
        public ERemoteInhibitLevel RemoteInhibitLevelInit { get; set; }

        #endregion

        /// <summary>
        ///     Writes the message to the display of the instrument.
        /// </summary>
        /// <param name="message">Message to be shown.</param>
        /// <param name="row">Location row where to write.</param>
        /// <param name="column">Location column, where the writing starts.</param>
        private void Display(string message, int row = 1, int column = 1)
        {
            if (string.IsNullOrEmpty(message))
                throw new ArgumentException("message", nameof(message));
            if (row <= 0 || 8 < row)
                throw new ArgumentOutOfRangeException(nameof(row));
            if (column <= 0 || 40 < column)
                throw new ArgumentOutOfRangeException(nameof(column));

            var buffer = ScpiQuery("DISP:MODE?");
            if(!buffer.Contains("TEXT"))
                ScpiCommand("DISP:MODE TEXT");

            ScpiCommand("DISP:TEXT:LOC "+ row + "," + column);

            buffer = message.Replace("\n", string.Empty);
            buffer = buffer.Length > 41 - column ? buffer.Substring(0, 41 - column) : buffer.Substring(0, buffer.Length);
            ScpiCommand("DISP:TEXT \"" + buffer.ToUpper() + "\"");

            QueryErrWithExc(true, "Display: ");
        }

        /// <inheritdoc />
        public override double GetCurrentLimitDc()
        {
            return ScpiQuery<double>("CURR?");
        }

        /// <inheritdoc />
        public override double GetVoltageDcProtectionLimitLower()
        {
            Log.Warning("WARNING! Ametek do not support separated VoltageDcProtectionLimit:Low/High settings. Both Low/High-commands reads same High-limit value.");
            var result = ScpiQuery<double>("VOLT:PROT?");
            return -1.0 * result;
        }

        /// <inheritdoc />
        public override double GetVoltageDcProtectionLimitUpper()
        {
            var result = ScpiQuery<double>("VOLT:PROT?");
            return result;
        }

        /// <inheritdoc />
        public override EState GetCurrentProtectionStatus()
        {
            CheckIfConnected();

            var response = ScpiQuery<int>("STAT:QUES:COND?");

            if ( (response & (int) EQuestionableStatRegBitMask.OverCurrent) > 0 ||
                 (response & (int) EQuestionableStatRegBitMask.RmsCurrent)  > 0 )
                return EState.On;

            return EState.Off;
        }

        /// <inheritdoc />
        public override EState GetVoltageDcProtectionState()
        {
            var conditionRegState = ScpiQuery<int>("STAT:QUES:COND?");
            return (conditionRegState & (int) EQuestionableStatRegBitMask.OverVoltage) == (int) EQuestionableStatRegBitMask.OverVoltage ?
                    EState.On : EState.Off;
        }

        /// <inheritdoc />
        public override double GetVoltageLevelDc()
        {
            return ScpiQuery<double>("VOLT:DC?");
        }

        /// <inheritdoc />
        public override EVoltageMode GetVoltageMode()
        {
            var readModeStat = ScpiQuery<string>("MODE?");

            switch (readModeStat)
            {
                case "AC":
                    return EVoltageMode.Ac;

                case "DC":
                    return EVoltageMode.Dc;

                //  case "ACDC": NO need yet. Will be added when need occurs
                // return EVoltageMode.AcDc;

                default:
                    throw new InvalidOperationException("GetVoltageMode query answer(" + readModeStat + "). " +
                                                        "Wanted answers: AC or DC"); // AC, DC or ACDC"); NO need yet. Will be added when need occurs
            }
        }

        /// <inheritdoc />
        public override double GetVoltageRange()
        {
            CheckIfConnected();
            var result = ScpiQuery<double>("VOLT:RANG?");
            return result;    // AMETEK AST1501 AC Ranges: RangeLoV = 200 / RangeHiV = 400
        }                     // AMETEK AST1501 DC Ranges: RangeLoV = 250 / RangeHiV = 500

        /// <inheritdoc />
        public override double MeasureCurrentDc()
        {
            var result = ScpiQuery<double>("MEAS:CURR:DC?");
            QueryErrWithExc(true, "MeasureCurrentDc: " + result);
            return result;
        }

        /// <inheritdoc />
        public override double MeasurePeakCurrent()
        {   // Ref: *Ametek Programming reference manual M330100-01 Rev D March 2018 page.57
            // To update the value with every measurement a peak current reset command should be used prior to the peak measurements.
            ScpiCommand("MEAS:CURR:AMPL:RES"); // Resets the peak current measurement data to zero.

            var samplePeriodMs = ScpiQuery<double>("SENS:SWE:TINT?") / 1000.0;     // us => ms
            var sampleTimeMs = Convert.ToInt32(samplePeriodMs * 4096.0 * 2.0 + 1); // Current is sampled over 2 measurement acquisition of 4096 data points.
                             // Using upper query + calculation, the sampling time will be practically near same as Keysight 680x, which makes this automatically.
            Log.Info("Reset the peak current measurement data to zero, done. Sampling time(" + sampleTimeMs + " mS) to collect measurement data.");
            Thread.Sleep( sampleTimeMs);

            var result = ScpiQuery<double>("MEAS:CURR:AMPL:MAX?");
            QueryErrWithExc(true, "MeasurePeakCurrent: " + result);
            return result;
        }

        /// <inheritdoc />
        public override double MeasureReactivePowerVAR()
        {
            throw new NotImplementedException(); // Ametek do not support this feature
        }

        /// <inheritdoc />
        public override double MeasureVoltageDc()
        {
            var result = ScpiQuery<double>("MEAS:VOLT:DC?");
            QueryErrWithExc(true, "MeasureVoltageDc: " + result);
            return result;
        }

        /// <inheritdoc />
        public override void Open()
        {
            if (IsConnected) throw new InvalidOperationException("Psu is already connected!");
            base.Open();

            // IDN query
            const string idWanted = "AMETEK Programmable Power,AST1501";
            var buffer = IdnString;
            Log.Info("ID-query responses: " + buffer);
            if (!buffer.Contains(idWanted))
            {
                Log.Error("Error Wanted ID is: " + idWanted);
                throw new ApplicationException("Invalid ID-query response(" + buffer + "). Wanted(" + idWanted + ")");
            }
            Display(buffer);

            // Voltage Sense setting
            Display("Set Voltage Sense to " + VoltageSenseInit);
            ScpiCommand("PONS:SENS " + VoltageSenseInit);
            WaitForOperationComplete();

            var voltageSenseRead  = ScpiQuery<ESense>("PONS:SENS?");
            buffer = "Voltage Sense(" + voltageSenseRead + "). Wanted(" + VoltageSenseInit + ")";
            if (voltageSenseRead != VoltageSenseInit)
            {
                Log.Error("Invalid " + buffer);
                Display("Invalid " + buffer);
                throw new ApplicationException("Invalid " + buffer);
            }
            Log.Info(buffer);

            // Voltage Range power on state settings
            ScpiCommand("PONS:VRAN " + VoltRangeInit); // Power on range = after reset too.
            WaitForOperationComplete();

            // Reset
            Display("Reset running.");
            Reset();
            Log.Info("AcPsuAmetek Reset ready.");

            // States after reset
            Display("Check settings after reset");

            // Voltage Range power on check
            var voltRangeRead = ScpiQuery<int>("PONS:VRAN?");
            if (voltRangeRead != VoltRangeInit)
            {
                buffer = "Power on Voltage Range(" + voltRangeRead + "). Wanted(" + VoltRangeInit + ")";
                Log.Error("Invalid " + buffer);
                Display("Invalid " + buffer);
                throw new ApplicationException("Invalid " + buffer);
            }

            // Voltage Range check
            voltRangeRead = Convert.ToInt32(GetVoltageRange());
            buffer = "Running Voltage Range(" + voltRangeRead + "). Wanted(" + VoltRangeInit + ")";
            if (voltRangeRead != VoltRangeInit)
            {
                Log.Error("Invalid " + buffer);
                Display("Invalid " + buffer);
                throw new ApplicationException("Invalid " + buffer);
            }
            Log.Info(buffer);

            // Voltage Range running settings
            ReadRangCritLimits(VoltRangeInit,
                Convert.ToInt32(EVoltageRangeAmetekAc.RangeLoV), Convert.ToInt32(EVoltageRangeAmetekAc.RangeHiV));
            SetVoltageRange(VoltRangeInit);            // Running range. This is possible to change in test

            // Sense state after reset
            voltageSenseRead = ScpiQuery<ESense>("PONS:SENS?");
            voltRangeRead = (int)GetVoltageRange();

            // RI (Remote Inhibit) state check
            var remoteInhibitLevelRead = ERemoteInhibitLevel.Low;
            var remoteInhibitModeRead = ScpiQuery<ERemoteInhibitMode>("OUTP:RI:MODE?");
            if (RemoteInhibitModeInit != ERemoteInhibitMode.Off)
                remoteInhibitLevelRead = ScpiQuery<ERemoteInhibitLevel>("OUTP:RI:LEV?");

            ScpiCommand("DISP:MODE NORM");
            if (voltageSenseRead == VoltageSenseInit && voltRangeRead == VoltRangeInit && remoteInhibitModeRead == RemoteInhibitModeInit
                && RemoteInhibitModeInit == ERemoteInhibitMode.Off)
            {
                Log.Info("Sense(" + voltageSenseRead + ") Range(" + voltRangeRead + ") Inhibit(" + remoteInhibitModeRead + ")");
                return;
            }

            if (voltageSenseRead == VoltageSenseInit && voltRangeRead == VoltRangeInit && remoteInhibitModeRead == RemoteInhibitModeInit
               && RemoteInhibitModeInit != ERemoteInhibitMode.Off && remoteInhibitLevelRead == RemoteInhibitLevelInit)
            {
                Log.Info("Sense(" + voltageSenseRead + ") Range(" + voltRangeRead + ") Inhibit(" + remoteInhibitModeRead + " " + remoteInhibitLevelRead + ")");
                return;
            }

            // ERRor: Setup went wrong. Log setup states ******************************************
            Log.Info( "Voltage Sense(" + voltageSenseRead + "). Wanted(" + VoltageSenseInit + ")"
                                      + (voltageSenseRead == VoltageSenseInit ? ".":" ==> Error"));

            Log.Info( "Voltage Range(" + voltRangeRead + "). Wanted(" + VoltRangeInit + ")"
                                      + (voltRangeRead == VoltRangeInit ? "." : " ==> Error"));

            voltRangeRead = ScpiQuery<int>("PONS:VRAN?");
            Log.Info("Voltage PowerOn Range(" + voltRangeRead + "). Wanted(" + VoltRangeInit + ")"
                                             + (voltRangeRead == VoltRangeInit ? "." : " ==> Error"));

            if (RemoteInhibitModeInit == ERemoteInhibitMode.Off)
            {
                Log.Info( "Voltage Inhibit(" + remoteInhibitModeRead + "). Wanted(" + RemoteInhibitModeInit + ")"
                                            + (remoteInhibitModeRead == RemoteInhibitModeInit ? "." : " ==> Error"));

                throw new ApplicationException("Error with settings after reset " +
                                 "Sense(" + voltageSenseRead + ") Range(" + voltRangeRead + ") Inhibit(" + remoteInhibitModeRead + "). \n" +
                          "Wanted Sense(" + VoltageSenseInit + ") Range(" + VoltRangeInit + ") Inhibit(" + RemoteInhibitModeInit + ")");
            }

            Log.Info("Voltage Inhibit mode(" + remoteInhibitModeRead + "). Wanted(" + RemoteInhibitModeInit+ ")"
                                            + (remoteInhibitModeRead == RemoteInhibitModeInit ? "." : " ==> Error"));

            Log.Info("Voltage Inhibit Level(" + remoteInhibitLevelRead + "). Wanted(" + RemoteInhibitLevelInit+ ")"
                                             + (remoteInhibitLevelRead == RemoteInhibitLevelInit ? "." : " ==> Error"));

            throw new ApplicationException("Error with settings after reset " +
                             "Sense(" + voltageSenseRead + ") Range(" + voltRangeRead + ") Inhibit(" + remoteInhibitModeRead + " " + remoteInhibitLevelRead + "). \n" +
                      "Wanted Sense(" + VoltageSenseInit + ") Range(" + VoltRangeInit + ") Inhibit(" + RemoteInhibitModeInit + " " + RemoteInhibitLevelInit + ")");
        }

        /// <inheritdoc />
        public override void Reset()
        {
            base.Reset();
            WaitForOperationComplete();

            // Remote Inhibit setup
            var remoteInhibitModeRead = ScpiQuery<ERemoteInhibitMode>("OUTP:RI:MODE?"); // Typical "LIVE" after reset
            if (remoteInhibitModeRead != RemoteInhibitModeInit)
            {
                Log.Info("RI-Mode after reset Read(" + remoteInhibitModeRead + ") Initial(" + RemoteInhibitModeInit + ") => Set initial next.");
                ScpiCommand("OUTP:RI:MODE " + RemoteInhibitModeInit);
            }

            if (RemoteInhibitModeInit != ERemoteInhibitMode.Off)
            {
                var remoteInhibitLevelRead = ScpiQuery<ERemoteInhibitLevel>("OUTP:RI:LEV?");
                if (remoteInhibitLevelRead != RemoteInhibitLevelInit)
                {
                    Log.Info("RI-Level after reset Read(" + remoteInhibitLevelRead + ") Initial(" + RemoteInhibitLevelInit + ") => Set initial next.");
                    ScpiCommand("OUTP:RI:LEV " + RemoteInhibitLevelInit);
                }
            }

            QueryErrWithExc(true, "Reset+RemoteInhibitSet");

            // reset can change mode, so CritLimits need to run again
            RangeLowSetVal = 0;
            RangeHiSetVal = 0;
            ReadRangCritLimits(VoltRangeInit, Convert.ToInt32(EVoltageRangeAmetekAc.RangeLoV), Convert.ToInt32(EVoltageRangeAmetekAc.RangeHiV));

            ClearOutputProtection();
        }

        /// <inheritdoc />
        public override int SelfTest()
        {
            const int timeOutSelfTmS = 12000; // Typical runtime is ~8s, Ametek AST1501
            var ioTimeoutOld = IoTimeout;     // Read IoTimeout to be returned later
            if (timeOutSelfTmS > IoTimeout)
                IoTimeout = timeOutSelfTmS;   // Set longer timeout, If need to wait longer than IoTimeout

            Display("Selftest running.");
            var timeStampStart = DateTime.Now;      // Runtime measurement start point

            // Run test
            var result = ScpiQuery<int>("*TST?");
            WaitForOperationComplete();
            IoTimeout = ioTimeoutOld;               // Revert timeout

            //Calculate runtime
            Log.Debug("SelfTest measured runtime/timeout: " +
                       Convert.ToInt32((DateTime.Now - timeStampStart).TotalMilliseconds) +
                      "/" + timeOutSelfTmS + "ms.");
            ScpiCommand("DISP:MODE NORM");

            //  Result
            if (result == 0)
                 Log.Info("SelfTest Passed");
            else Log.Info("SelfTest Failed. Result: " + result);
            return result;
        }

        /// <inheritdoc />
        public override void SetCurrent(double currentAmps)
        {
            if (currentAmps < 0)
                throw new ArgumentOutOfRangeException(nameof(currentAmps));

            ScpiCommand("CURR " + currentAmps.ToString("#.000", CultureInfo.InvariantCulture));
            QueryErrWithExc(true, "SetCurrent: " + currentAmps);
        }

        /// <inheritdoc />
        public override void SetCurrentDc(double currentAmps)
        {
            if (currentAmps < 0)
                throw new ArgumentOutOfRangeException(nameof(currentAmps));

            ScpiCommand("CURR " + currentAmps.ToString("#.000", CultureInfo.InvariantCulture));
            QueryErrWithExc(true, "SetCurrent: " + currentAmps);
        }

        /// <inheritdoc />
        public override void SetVoltageDc(double voltageDc)
        {
            if (voltageDc < -500.0 || 500.0 < voltageDc)
                throw new ArgumentOutOfRangeException(nameof(voltageDc));

            ScpiCommand("VOLT:DC " + voltageDc.ToString("#.000", CultureInfo.InvariantCulture));
            QueryErrWithExc(true, "SetVoltageDc: " + voltageDc);
        }

        /// <inheritdoc />
        public override void SetVoltageDcProtectionLimitLower(double voltageDcLimitLow)
        {
            Log.Warning("WARNING! Ametek do not support separated VoltageDcProtectionLimit:Low/High settings. Both Low/High-commands sets same limit value.");
            ScpiCommand("VOLT:PROT " + Math.Abs(voltageDcLimitLow).ToString("#.000", CultureInfo.InvariantCulture));
            QueryErrWithExc(true, "SetVoltageDcProtectionLimitLower: " + voltageDcLimitLow);
        }

        /// <inheritdoc />
        public override void SetVoltageDcProtectionLimitUpper(double voltageDcLimitUpp)
        {
            ScpiCommand("VOLT:PROT " + voltageDcLimitUpp.ToString("#.000", CultureInfo.InvariantCulture));
            QueryErrWithExc(true, "SetVoltageDcProtectionLimitUpper: " + voltageDcLimitUpp);
        }

        /// <inheritdoc />
        public override void SetVoltageDcProtectionState(EState onOff)
        {
            if (!Enum.IsDefined(typeof(EState), onOff))
                throw new InvalidEnumArgumentException(nameof(onOff), (int) onOff, typeof(EState));

            if (onOff != EState.Off)
                return;                   // Ametek do not support protection off state.
            ScpiCommand("VOLT:PROT MAX"); // Set max level instead of Off-state.
            QueryErrWithExc(true, "SetVoltageDcProtectionState: " + onOff);
        }

        /// <inheritdoc />
        public override void SetVoltageMode(EVoltageMode voltageMode)
        {
            if (!Enum.IsDefined(typeof(EVoltageMode), voltageMode))
                throw new InvalidEnumArgumentException(nameof(voltageMode), (int) voltageMode, typeof(EVoltageMode));

            var modeOld = GetVoltageMode();
            if( modeOld != voltageMode)
            {
                ScpiCommand("MODE " + voltageMode);
                RangeLowSetVal = 0;
                RangeHiSetVal = 0;

                switch (voltageMode)
                {
                    case EVoltageMode.Ac:
                        ReadRangCritLimits( VoltRangeInit,
                            Convert.ToInt32(EVoltageRangeAmetekAc.RangeLoV), Convert.ToInt32(EVoltageRangeAmetekAc.RangeHiV) );
                        break;

                    case EVoltageMode.Dc:
                        ReadRangCritLimits( VoltRangeInit == Convert.ToInt32(EVoltageRangeAmetekDc.RangeHiV)
                                ? Convert.ToInt32(EVoltageRangeAmetekDc.RangeHiV) : Convert.ToInt32(EVoltageRangeAmetekDc.RangeLoV),
                                  Convert.ToInt32(EVoltageRangeAmetekDc.RangeLoV), Convert.ToInt32(EVoltageRangeAmetekDc.RangeHiV) );
                        break;

                    //  case EVoltageMode.AcDc: NO need yet. Will be added when need occurs
                    // return ;

                    default:
                        throw new InvalidOperationException("Error VoltageMode(" + voltageMode + ") setting " +
                                                            "Supported: AC or DC"); // AC, DC or ACDC"); NO need yet. Will be added when need occurs
                }
            }

            QueryErrWithExc(true, "SetVoltageMode: " + voltageMode);
        }
    }
}
